﻿using System;
using System.Collections.Generic;
using System.Data.Services.Client;
using System.Linq;
using System.Net;
using System.Reflection;
using BackupObjects.Taging;
using Microsoft.Samples.ServiceHosting.PersonalWebSite;
using Microsoft.Samples.ServiceHosting.StorageClient;

namespace BackupObjects
{
    /// <summary>
    /// Base class for an entity that is persist on the table.
    /// </summary>
    /// <typeparam name="TItem">Front end entity.</typeparam>
    /// <typeparam name="TItemRow">Internal table entity.</typeparam>
    public abstract class BaseTableRepository<TItem, TItemRow>
        where TItem : class
        where TItemRow : TableStorageEntity, IIdentifiableEntity
    {
        protected static TableStorage TableStorage;
        protected static RetryPolicy TableRetry = ProviderRetryPolicies.RetryN(_numRetries, TimeSpan.FromSeconds(1));

        protected static StorageAccountInfo Account;

        /// <summary>
        /// The numbers of retries when an operation cannot be made.
        /// </summary>
        private static int _numRetries;

        public abstract string TableName
        {
            get;
        }

        /// <summary>
        /// Method used for conversation.
        /// </summary>
        /// <remarks>Use ConversationMethod</remarks>
        private static MethodInfo _conversionMethod;

        private static object _conversionMethodPadLock = new object();

        /// <summary>
        /// Get the conversation method used to convert from TItemRow to TItem.
        /// </summary>
        private static MethodInfo GetConversionMethodToItem
        {
            get
            {
                if (_conversionMethod == null)
                {
                    lock (_conversionMethodPadLock)
                    {
                        if (_conversionMethod == null)
                        {
                            Type sourceType = typeof(TItemRow);
                            Type targetType = typeof(TItem);

                            MethodInfo[] sourceinfos = sourceType.GetMethods
                    (BindingFlags.Public | BindingFlags.Static);
                            foreach (MethodInfo info in sourceinfos)
                            {
                                if ((info.Name == "op_Explicit" || info.Name == "op_Implicit")
                                    && info.ReturnType == targetType)
                                {
                                    _conversionMethod = info;
                                    break;
                                }
                            }
                            if (_conversionMethod == null)
                            {
                                throw new NotImplementedException("Implicit conversion");
                            }
                        }
                    }
                }

                return _conversionMethod;
            }
        }

        private static MethodInfo _conversionRowMethod;

        private static object _conversionRowMethodPadLock = new object();

        /// <summary>
        /// Get the conversation method used to convert from TItem to TItemRow.
        /// </summary>
        private static MethodInfo GetConversionMethodToItemRow
        {
            get
            {
                if (_conversionRowMethod == null)
                {
                    lock (_conversionRowMethodPadLock)
                    {
                        if (_conversionRowMethod == null)
                        {
                            Type sourceType = typeof(TItemRow);
                            Type targetType = typeof(TItem);

                            MethodInfo[] sourceinfos = sourceType.GetMethods
                    (BindingFlags.Public | BindingFlags.Static);
                            foreach (MethodInfo info in sourceinfos)
                            {
                                if ((info.Name == "op_Explicit" || info.Name == "op_Implicit")
                                    && info.ReturnType == sourceType)
                                {
                                    _conversionRowMethod = info;
                                    break;
                                }
                            }
                            if (_conversionRowMethod == null)
                            {
                                throw new NotImplementedException("Implicit conversion");
                            }
                        }
                    }
                }

                return _conversionRowMethod;
            }
        }

        /// <param name="numRetries">The numbers of retries when an operation cannot be made.</param>
        protected BaseTableRepository(int numRetries = 3)
        {
            _numRetries = numRetries;
            Initialize();
        }

        /// <summary>
        /// Init the tag repository.
        /// </summary>
        private void Initialize()
        {
            if (TableStorage == null)
            {
                try
                {
                    Account = StorageAccountInfo.GetDefaultTableStorageAccountFromConfiguration();

                    TableStorage = TableStorage.Create(Account);
                    TableStorage.TryCreateTable(TableName);
                }
                catch (Exception exception)
                {
                    Log.Write(EventKind.Error, "Initialization of data service structures for tag storage failed!");
                    throw new Exception("Could not create or find backup table table  in the data service ", exception);
                }
            }
        }

        /// <summary>
        /// Save a TItem to repository.
        /// </summary>
        /// <param name="obj">The Tag to save.</param>
        ///<returns>The saved endtity, with the new id.</returns>
        public virtual TItem Save(TItem obj)
        {
            TItemRow objRow = (TItemRow)GetConversionMethodToItemRow.Invoke(obj, new[] { obj });

            //Determine if the object already exist on the repository.
            var isNew = objRow.Id==default(Guid) || Load(objRow.Id)==null;

            //Generate a new id, when the id have the default value.
            if (objRow.Id == Guid.Empty)
            {
                objRow.Id = Guid.NewGuid();
            }

            try
            {
                var context = TableStorage.GetDataServiceContext();
                if(!isNew)
                {
                    Delete(objRow.Id);
                }
                
                context.AddObject(TableName, objRow);                 
                context.SaveChanges();
            }
            catch (InvalidOperationException e)
            {
                HttpStatusCode status;
                // when retry policies are used we cannot distinguish between a conflict and success
                // so, in the case of a conflict, we just retrun success here
                if (TableStorageHelpers.EvaluateException(e, out status) && status == HttpStatusCode.Conflict)
                {
                    return null;
                }

                Log.Write(EventKind.Warning, "Could not add the {0} (ID={1}) to the table.", typeof(TItem).Name, objRow.Id);
                throw new Exception(string.Format("Failed to access the {0} table ", typeof(TItemRow).Name), e);
            }

            // ReSharper disable PossibleInvalidCastException
            return (TItem)GetConversionMethodToItem.Invoke(objRow, new[] { objRow });
            // ReSharper restore PossibleInvalidCastException
        }

        public void Delete(Guid id)
        {
            var context = TableStorage.GetDataServiceContext();
            List<TItemRow> itemRows = LoadById(id, context);

            if (itemRows.Count > 1)
            {
                Log.Write(
                    EventKind.Warning,
                    string.Format("There are multiple {0} rows with id {1}", typeof(TItemRow), id));
            }

            if (itemRows.Count == 0)
            {
                Log.Write(
                    EventKind.Warning,
                    string.Format("No {0} row found with id {1}", typeof(TItemRow).Name, id));
            }

            if (itemRows.Count > 0)
            {
                itemRows.ForEach(context.DeleteObject);
                context.SaveChanges();
            }
        }

        /// <summary>
        /// Load all items with a given id.
        /// </summary>
        /// <param name="id">The given id.</param>
        /// <param name="context">The table context.</param>
        /// <returns>The list of tag rows.</returns>
        private List<TItemRow> LoadById(Guid id, TableStorageDataServiceContext context)
        {
            string sId = id.ToString();
            var query = DataServiceQuery(context??TableStorage.GetDataServiceContext()).Where(item => item.RowKey == sId);

            TableStorageDataServiceQuery<TItemRow> dataServiceQuery =
                new TableStorageDataServiceQuery<TItemRow>(query as DataServiceQuery<TItemRow>, TableRetry);
            return dataServiceQuery.ExecuteWithRetries().ToList();
        }

        /// <summary>
        /// Get the data query that can be used for custom queries.
        /// </summary>
        /// <param name="context">The table storage context.</param>
        /// <returns>The data query</returns>
        public DataServiceQuery<TItemRow> DataServiceQuery(TableStorageDataServiceContext context = null)
        {
            if (context == null)
            {
                context = TableStorage.GetDataServiceContext();
            }
            return context.CreateQuery<TItemRow>(TableName);
        }

        /// <summary>
        /// Load a tag by id.
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public TItem Load(Guid id)
        {
            var items = LoadById(id, TableStorage.GetDataServiceContext());


            if (items.Count == 0)
            {
                return null;
            }

            return (TItem)GetConversionMethodToItem.Invoke(items[0], new[] { items[0] });
        }

        /// <summary>
        /// Load a collection of tags based on id.
        /// </summary>
        /// <param name="ids">The list of ids</param>
        /// <returns>The list of tags for the given ids.</returns>
        public List<TItem> Load(IList<Guid> ids)
        {
            if(ids==null)
            {
                return null;
            }
            List<TItem> tags = new List<TItem>();
            foreach (var id in ids)
            {
                TItem tag = Load(id);
                if (tag != null)
                {
                    tags.Add(tag);
                }
            }

            return tags;
        }

        /// <summary>
        /// Load all tags from repository.
        /// </summary>
        /// <returns></returns>
        public List<Tag> LoadAll()
        {
            var context = TableStorage.GetDataServiceContext();
            IEnumerable<TagRow> query = context.CreateQuery<TagRow>(TableName);
            TableStorageDataServiceQuery<TagRow> q = new TableStorageDataServiceQuery<TagRow>(query as DataServiceQuery<TagRow>, TableRetry);
            var tagRows = q.ExecuteAllWithRetries().ToList();

            return tagRows.ConvertAll<Tag>(item => item);
        }
    }
}